﻿using System;
using System.Diagnostics;
using System.Linq;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using NMapper.Utils;

namespace NMapper.Mappings
{
    public class EntityMapping<TDatabase, TParentDbEntity, TChildDbEntity, TParentOutput, TChildOutput> : IValueMapping<TDatabase, TParentDbEntity, TChildDbEntity, TParentOutput, TChildOutput>
        where TChildDbEntity : class
        where TDatabase : IDisposable
        where TChildOutput : class
    {
        protected Expression<Func<MapContext<TDatabase>, TParentDbEntity, TChildDbEntity>> dbSelector;
        protected Func<MapContext<TDatabase>, TParentDbEntity, TChildDbEntity> dbSelectorFunc;
        protected Expression<Func<TParentOutput, TChildOutput>> outputSelector;
        protected Func<TParentOutput, TChildOutput> outputSelectorFunc;
        protected Mapper<TDatabase, TChildDbEntity, TChildOutput> mapper;
        protected PropertyInfo property;

        public EntityMapping(Expression<Func<MapContext<TDatabase>, TParentDbEntity, TChildDbEntity>> dbSelector, Expression<Func<TParentOutput, TChildOutput>> outputSelector, Mapper<TDatabase, TChildDbEntity, TChildOutput> mapper)
        {
            this.dbSelector = dbSelector;
            dbSelectorFunc = dbSelector.Compile();
            this.outputSelector = outputSelector;
            outputSelectorFunc = outputSelector.Compile();
            this.mapper = mapper;
        }

        public Mapper<TDatabase, TChildDbEntity, TChildOutput> Mapper
        {
            get { return mapper; }
        }

        public PropertyInfo TransientProperty
        {
            get { return property; }
        }

        LambdaExpression IDbSelector<TDatabase>.DbSelectorExpression
        {
            get { return dbSelector; }
        }

        LambdaExpression IOutputSelector.OutputSelectorExpression
        {
            get { return outputSelector; }
        }

        Func<MapContext<TDatabase>, object, object> IDbSelector<TDatabase>.DbSelector
        {
            get { return (mapContext, x) => DbSelector(mapContext, (TParentDbEntity)x); }
        }

        public Func<MapContext<TDatabase>, TParentDbEntity, TChildDbEntity> DbSelector
        {
            get { return dbSelectorFunc; }
        }

        Func<object, object> IOutputSelector.OutputSelector
        {
            get { return x => OutputSelector((TParentOutput)x); }
        }

        public Func<TParentOutput, TChildOutput> OutputSelector
        {
            get { return outputSelectorFunc; }
        }

        public PropertyInfo DbProperty
        {
            get { return dbSelector.GetPropertyInfo(); }
        }

        public void DefineTransientType(ModuleBuilder module, TypeBuilder type)
        {
            mapper.Initialize();
            var propertyName = NameHelpers.SuggestName(dbSelector, outputSelector);
            property = type.DefineProperty(propertyName, mapper.TransientType);
        }

        public void DefineTransientInitialization(MapContext<TDatabase> mapContext, Type transientType, List<MemberBinding> initializers, ParameterExpression parameterExpression)
        {
            property = transientType.GetProperty(property.Name);

            var lambdaBinder = new LambdaBinder();
            var subSelector = mapper.CreateTransientSelector(mapContext);
            var simpleDbSelector = dbSelector.PartialApplication(Expression.Constant(mapContext), null).Simplify();
            var newTarget = lambdaBinder.BindBody(simpleDbSelector, parameterExpression);
            var boundSubSelector = lambdaBinder.BindBody(subSelector, newTarget);

            var nullChecker = new LambdaNullChecker();
            Expression boundSubSelectorWithNullCheck = nullChecker.AddNullCheck(newTarget, boundSubSelector);
            
            var initializer = Expression.Bind(property.GetSetMethod(), boundSubSelectorWithNullCheck);
            initializers.Add(initializer);
        }

        void IMapping<TDatabase>.OnTransientInitialized(MapContext<TDatabase> mapContext, Array transient)
        {
            foreach (var item in transient)
            {
                var value = property.GetValue(item, null);
                mapper.OnTransientInitialized(mapContext, value != null ? new[] { value } : new object[0]);                
            }
        }

        public void MapToOutput(MapContext<TDatabase> mapContext, object input, object output)
        {
            var outputProperty = outputSelector.GetPropertyInfo();
            var value = property.GetValue(input, null);

            if (value != null)
                value = mapper.TransformToOutput(mapContext, new[] { value })[0];

            outputProperty.SetValue(output, value, null);
        }

        public virtual void MapToInput(MapContext<TDatabase> mapContext, object output, object input)
        {
            var existingEntity = DbSelector(mapContext, (TParentDbEntity)input);
            var existingEntityId = existingEntity != null ? (int?)Mapper.PrimaryKey.DbSelector(mapContext, existingEntity) : null;

            var outputValue = OutputSelector((TParentOutput)output);

            var dbProperty = DbProperty;
            if (dbProperty != null)
            {
                var entity = outputValue == null ? null : mapper
                    .TransformToInput(mapContext, new[] { outputValue }, ids => existingEntity != null ? new[] { existingEntity } : new TChildDbEntity[0])
                    .SingleOrDefault();                    
                if (entity == null)
                    dbProperty.SetValue(input, null, null);
                else if (existingEntityId == null || (int)mapper.PrimaryKey.DbSelector(mapContext, entity) != existingEntityId)
                    dbProperty.SetValue(input, entity, null);
            }
        }

        public override string ToString()
        {
            return string.Format("EntityMapping({0}, {1}, {2})", dbSelector, outputSelector, mapper);
        }
    }
}